POST /v2/products
[!info] 概述 创建新的 ANY_DAY_TICKET(任意日门票)产品。在购买时才绑定到具体活动,而非创建时。
[!tip] 核心特性
- 自动 Event Catalog:自动使用用户的活动目录
- 无 Event 绑定:
Product.eventId始终为null- 灵活 Event 选择:购买时通过
variantInfo.eventId动态绑定- 手续费计算:自动计算交易手续费、平台费用、税费
请求信息
请求地址
POST /v2/products
请求头 (Headers)
| Header | 类型 | 必填 | 说明 |
|---|---|---|---|
authorization |
string |
✅ | Bearer Token (JWT) |
content-type |
string |
✅ | application/json |
timezone |
string |
❌ | 用户时区 (如 Asia/Shanghai) |
from |
string |
❌ | 客户端标识 (如 client) |
请求体 (Body)
请求参数结构
interface CreateProductV2Dto {
// ========== 必填字段 ==========
title: string; // 产品标题
status: ProductStatus; // 产品状态
listingType: ProductType.ANY_DAY_TICKET; // 产品类型(必须为 ANY_DAY_TICKET)
deliveryMethod: ProductDeliveryMethod; // 交付方式(QR_CODE / IN_PERSON / THIRD_PARTY_ISSUED)
images: CreateProductImage[]; // 产品图片数组
// ========== 可选字段 ==========
// 商家/目录
merchantId?: string; // 商家 ID(V2 可选,自动解析为 Event Catalog)
// 内容
bodyHtml?: string; // HTML 正文
bodyJson?: string; // Lexical JSON 正文
// 选项与变体
options?: CreateProductOption[]; // 规格选项(默认自动生成)
variants: CreateProductVariant[]; // 变体数组(至少 1 个)
// 配送设置
shippingType?: ShippingType; // 运输类型(默认 NO_SHIPPING_REQUIRED)
deliveryTime?: [number, number]; // 配送时间范围(天)
additionalShippingFee?: number; // 额外运费
autoFulfill?: boolean; // 自动履约(默认 false)
shippingOptions?: ProductShippingOptionRequest[]; // 运费选项
shippingNote?: string; // 配送说明
// 第三方配送消息 (KAT-9452)
thirdPartyDeliveryMessage?: string; // 第三方配送消息(最大 500 字符)
// 状态与标记
followExternalStatus?: boolean; // 是否跟随外部状态(默认 true)
isUsed?: ProductUseStatus; // 产品使用状态(NWT / USED)
isFeatured?: boolean; // 是否精选
featuredScore?: number; // 精选评分
// 税收配置
taxEnable?: boolean; // 是否启用税收
taxJarCategory?: string; // TaxJar 分类
overrideEventTax?: boolean; // 是否覆盖 Event 税收 (KAT-10224)
customTaxRate?: number | null; // 自定义税率(0-100,null 表示禁用)
// 佣金
commissionRate?: number; // 佣金比例
catalogCommissionRate?: number; // 目录佣金比例
// 链接
links?: object; // 产品链接
// ANY_DAY_TICKET 特有字段
// eventId 始终为 null(购买时才绑定)
eventId?: string; // ❌ 不支持(ANY_DAY_TICKET 无需提供)
// 门票筛选配置 (KAT-10412)
atDoorTicketConfig?: AtDoorTicketConfig;
// 生命周期状态
lifecycleStatus?: ProductLifecycleStatus; // 生命周期状态(默认 NORMAL)
stopSellingAfterDisplay?: string; // 停止销售时间显示
// 产品表单 (KAT-8634)
isProductFormEnabled?: boolean; // 是否启用产品表单
productForm?: CreateUserContactFormRequest; // 产品表单配置
// 同步设置
excludeFromPostSync?: boolean; // 排除自动同步(默认 false)
// 终端销售
allowAtDoorSales?: boolean; // 允许终端销售(默认 false)
}
CreateProductImage 结构
interface CreateProductImage {
id: string; // 图片 ID(Cloudinary public_id)
src: string; // 图片 URL
width: number; // 图片宽度
height: number; // 图片高度
mediaType: MediaType; // 媒体类型(IMAGE / VIDEO)
source: ProductImageSource; // 图片来源
position: number; // 图片位置
setThumbnail?: boolean; // 是否设为缩略图
origin?: { // 来源信息
id: string;
width: number;
height: number;
position: number;
};
productImageType?: ProductImageType; // 图片类型(PRODUCT / VARIANT)
}
CreateProductOption 结构
interface CreateProductOption {
name: string; // 选项名称(如 "Title", "Size")
values: string[]; // 选项值数组(如 ["Default Title"])
images?: any[]; // 选项图片
}
CreateProductVariant 结构
interface CreateProductVariant {
// ========== 必填字段 ==========
price: string | number; // 价格(字符串或数字)
inventoryQuantity: number; // 库存数量
option?: { // 规格选项(自动生成 title)
option1?: string;
option2?: string;
option3?: string;
};
// ========== ANY_DAY_TICKET 专属字段 ==========
ticketPrice?: number; // 票面价格(用于手续费计算)
fees?: number; // 手续费(自动计算)
transactionFee?: { // 交易手续费明细
platformFee: number; // 平台费用
customFee: number; // 自定义费用
customFeeBreakdown?: { // 费用明细
TAX: {
unitFixedFee: number; // 固定税费
unitPercentageFee: number; // 百分比税费
itemFee?: number; // 项目费用
}
};
transactionItemFee: number; // 总手续费
};
// ========== 可选字段 ==========
priceAnchor?: string | number; // 锚定价格(用于划线显示)
compareAtPrice?: string | number; // 比较价格
// 购买数量限制 (KAT-9346)
isMinPurchaseQuantityEnabled?: boolean;
minPurchaseQuantity?: number;
isMaxPurchaseQuantityEnabled?: boolean;
maxPurchaseQuantity?: number;
isPackSizeEnabled?: boolean;
packSize?: number;
// SKU
sku?: string;
// 图片
imageId?: string;
}
AtDoorTicketConfig 结构 (KAT-10412)
interface AtDoorTicketConfig {
includeTitleText?: string | null; // 包含指定标题的活动才显示
excludeTitleText?: string | null; // 排除指定标题的活动
eventSelectionWindowHours?: number | null; // 事件选择时间窗口(小时)
}
CreateUserContactFormRequest 结构 (KAT-8634)
interface CreateUserContactFormRequest {
title: string; // 表单标题
subtitle?: string; // 表单副标题
showImage?: boolean; // 是否显示图片
contactFormFields: ContactFormField[]; // 表单字段数组
}
interface ContactFormField {
title: string; // 字段标题
field: UserContactFormEnum; // 字段类型
required: boolean; // 是否必填
description?: string; // 字段描述
tooltip?: string; // 字段提示
extensions?: object; // 字段扩展配置
}
响应结构
响应格式
ProductResponse
响应字段说明
见 [[GET /v2/products/any-day-tickets]] 响应字段说明
成功示例
请求示例 (cURL)
curl 'https://release.katana-api.1m.app/v2/products' \
-H 'accept: application/json, text/plain, */*' \
-H 'authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...' \
-H 'content-type: application/json' \
-H 'timezone: Asia/Shanghai' \
--data-raw '{
"merchantId": "bf1d0f03-03c7-4c6d-b886-80eea723eab3",
"shippingType": "NO_SHIPPING_REQUIRED",
"isFeatured": false,
"commissionRate": 0,
"priceSyncImported": false,
"additionalShippingFee": 0,
"returnPolicyApplied": false,
"images": [{
"id": "80f43627-4bef-49b0-934b-cd63d49f8787",
"height": 1000,
"width": 1000,
"src": "https://res.cloudinary.com/dr9io1zjv/v1755656006/uploaded_images/pt4zctae8zwv8jrljrf7.png",
"mediaType": "IMAGE",
"source": "PRE_DESIGNED",
"position": 1,
"setThumbnail": true
}],
"title": "At-Door General Admission",
"bodyHtml": "<p>Valid for any upcoming event</p>",
"status": "ACTIVE",
"isUsed": "NWT",
"taxJarCategory": "",
"platform": "PEAR",
"autoFulfill": true,
"options": [{
"name": "Title",
"values": ["Default Title"],
"images": []
}],
"variants": [{
"inventoryQuantity": 100,
"price": 50,
"priceAnchor": 0,
"option": { "option1": "Default Title" },
"ticketPrice": 50,
"fees": 5.50,
"transactionFee": {
"platformFee": 1.00,
"customFee": 0,
"customFeeBreakdown": {
"TAX": {
"unitFixedFee": 0,
"unitPercentageFee": 0.1,
"itemFee": 0
}
},
"transactionItemFee": 5.50
}
}],
"listingType": "ANY_DAY_TICKET",
"catalogCommissionRate": 0,
"deliveryMethod": "QR_CODE",
"taxEnable": false,
"customTaxRate": 0,
"overrideEventTax": true,
"atDoorTicketConfig": {
"eventSelectionWindowHours": 24,
"includeTitleText": "",
"excludeTitleText": ""
},
"thirdPartyDeliveryMessage": "",
"bodyJson": "{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"Valid for any upcoming event\",\"type\":\"extended-text\",\"version\":1}],\"direction\":\"ltr\",\"format\":\"\",\"indent\":0,\"type\":\"katana-paragraph\",\"version\":1}]}}"
}'
响应示例
{
"id": "018eaabc-1234-5678-9012-fedcba987654",
"title": "At-Door General Admission",
"listingType": "ANY_DAY_TICKET",
"status": "ACTIVE",
"externalStatus": "ACTIVE",
"followExternalStatus": true,
"platform": "PEAR",
"catalog": {
"id": "bf1d0f03-03c7-4c6d-b886-80eea723eab3",
"name": "Event Catalog",
"catalogType": "EVENT"
},
"eventId": null,
"event": undefined,
"bodyHtml": "<p>Valid for any upcoming event</p>",
"bodyText": "Valid for any upcoming event",
"coverImage": {
"src": "https://res.cloudinary.com/dr9io1zjv/v1755656006/uploaded_images/pt4zctae8zwv8jrljrf7.png",
"mediaType": "IMAGE"
},
"variants": [
{
"id": "variant-uuid-1",
"position": 1,
"price": "50.00",
"inventoryQuantity": 100,
"title": "Default Title",
"option": { "option1": "Default Title" },
"fees": 5.50,
"transactionFee": {
"transactionItemFee": 5.50,
"platformFee": 1.00,
"customFee": 0,
"customFeeBreakdown": { "TAX": { "unitFixedFee": 0, "unitPercentageFee": 0.1 } }
}
}
],
"priceMin": 50.00,
"priceMax": 50.00,
"deliveryMethod": "QR_CODE",
"shippingType": "NO_SHIPPING_REQUIRED",
"autoFulfill": true,
"isAvailable": true,
"taxEnable": false,
"isFeatured": false,
"isUsed": "NWT",
"createdAt": "2026-03-02T01:00:00Z",
"updatedAt": "2026-03-02T01:00:00Z",
"isMultipleDaysPassEnabled": false,
"atDoorTicketConfig": {
"eventSelectionWindowHours": 24,
"includeTitleText": "",
"excludeTitleText": ""
},
"isProductFormEnabled": false,
"productForm": null
}
错误示例
400 Bad Request - 缺少必填字段
{
"statusCode": 400,
"message": ["title should not be empty", "variants must not be empty"],
"error": "Bad Request"
}
原因:缺少 title 或 variants 必填字段。
400 Bad Request - 不支持多日通票
{
"statusCode": 400,
"message": "ANY_DAY_TICKET does not support multi-day pass. Use TICKET type for multi-day passes.",
"error": "Bad Request"
}
原因:ANY_DAY_TICKET 不支持 isMultipleDaysPassEnabled。
400 Bad Request - 提供了 eventId
{
"statusCode": 400,
"message": "ANY_DAY_TICKET does not require eventId at creation time. Event binding happens at purchase time.",
"error": "Bad Request"
}
原因:ANY_DAY_TICKET 创建时不应该提供 eventId(在购买时才绑定)。
403 Forbidden - 权限不足
{
"statusCode": 403,
"message": "No permission",
"error": "Forbidden"
}
原因:用户不是 BUSINESS_PARTNER 角色。
400 Bad Request - 手续费计算不匹配
{
"statusCode": 400,
"message": "Variant fees or price calculation does not match the expected value",
"error": "Bad Request"
}
原因:前端传递的 fees 与后端计算的手续费不一致。
业务逻辑
核心流程
┌─────────────────────────────────────────────────────────────────┐
│ 1. Controller 层验证 │
│ ✅ 用户权限检查 (BUSINESS_PARTNER) │
│ ✅ 交付方式验证 (deliveryMethod + shippingType) │
│ ✅ 运费配置验证 (shippingOptions) │
└─────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────┐
│ 2. 自动解析商家 ID │
│ if ANY_DAY_TICKET → merchantId = Event Catalog ID │
│ eventCatalogService.ensureExists(userId) │
└─────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────┐
│ 3. Strategy 层验证 (AnyDayTicketProductStrategy) │
│ ✅ 标题必填 │
│ ✅ 至少一个变体 │
│ ✅ 目录类型必须是 EVENT │
│ ✅ eventId 必须为空 (或不提供) │
│ ✅ 不支持多日通票 │
│ ✅ 交付方式必须是 QR_CODE/IN_PERSON/THIRD_PARTY_ISSUED │
│ ✅ 精选数量限制 (max 50) │
│ ✅ atDoorTicketConfig 验证 (KAT-10412) │
│ ✅ 产品表单验证 (KAT-8634) │
│ ✅ 手续费验证 (checkTicketVariantPrices) │
└─────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────┐
│ 4. 数据增强 (enrichForCreate) │
│ - 设置 listingType = ANY_DAY_TICKET │
│ - eventId = undefined (始终为空) │
│ - deliveryMethod = QR_CODE (默认) │
│ - shippingType = NO_SHIPPING_REQUIRED │
│ - autoFulfill = true │
│ - taxEnable, overrideEventTax, customTaxRate │
│ - atDoorTicketConfig 默认值 │
│ - 产品表单字段 │
└─────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────┐
│ 5. 变体处理 (processVariantsForCreate) │
│ - 计算 ticketPrice (默认等于 price) │
│ - 计算手续费 (transactionFee, platformFee, customFee) │
│ - 税率优先级: overrideEventTax → User.globalEventTaxRate → 0% │
│ - 设置变体默认值 (inventoryManagement, inventoryPolicy 等) │
│ - 验证手续费与前端传入值一致 │
└─────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────┐
│ 6. 数据库事务 (Phase 1) │
│ prisma.$transaction(async (tx) => { │
│ // 创建 Product │
│ const product = await tx.product.create({ ... }); │
│ // 创建 PromoterProduct │
│ await tx.promoterProduct.create({ ... }); │
│ }) │
└─────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────┐
│ 7. Phase 2: 添加到 StoreFront 模块 (如果 moduleIds 提供) │
│ storeFrontModuleItemService.addItemToModulesAtBeginning() │
└─────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────┐
│ 8. Phase 3: 处理产品表单 (KAT-8634) │
│ productFormService.handleProductForm(isEnabled, form) │
└─────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────┐
│ 9. Phase 4: 后置钩子 (afterCreate) │
│ - 搜索引擎索引 (Bull Queue 异步) │
│ - 事件发布 (emit EventV2Created) │
│ - QR 码模板初始化 │
└─────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────┐
│ 10. 返回完整产品信息 │
│ productService.getProduct(product.id, userId) │
└─────────────────────────────────────────────────────────────────┘
关键设计决策
1. 自动 Event Catalog
- ANY_DAY_TICKET 必须在 Event Catalog 中创建
merchantId可选(V2 特性)- 系统自动调用
eventCatalogService.ensureExists(userId)获取/创建 Event Catalog
2. 无 Event 绑定
Product.eventId始终为null- Event 绑定通过
OrderLineItem.variantInfo.eventId在购买时存储 - 前端需调用
GET /product-event/v2/upcoming获取可选活动列表
3. 税率计算
税率优先级:
1. overrideEventTax = true → 使用 customTaxRate (null = 0%)
2. overrideEventTax = false → 使用 User.globalEventTaxRate
3. 默认 → 0%
手续费计算:
- transactionItemFee = platformFee + customFee
- platformFee = 1.00 (固定)
- customFee = TAX 税费
4. 不支持多日通票
- 请求中包含
isMultipleDaysPassEnabled = true会抛出错误 - 需要多日通票功能应使用
listingType: TICKET
5. 手续费验证
- 前端传递的
fees必须与后端计算一致 - 验证函数:
checkTicketVariantPrices() - 容差范围: ±0.01(考虑浮点数精度)
注意事项
1. 权限要求
- 用户必须是 BUSINESS_PARTNER 角色
- 未登录用户返回
401 Unauthorized - 权限不足返回
403 Forbidden
2. Event Catalog 自动创建
- 首次创建 ANY_DAY_TICKET 时自动创建 Event Catalog
- Event Catalog 的
catalogType = 'EVENT' - 无需手动创建商家/目录
3. 交付方式限制
| 交付方式 | 说明 |
|---|---|
QR_CODE |
✅ 推荐 - 二维码核销 |
IN_PERSON |
✅ 支持 - 现场交付 |
THIRD_PARTY_ISSUED |
✅ 支持 - 第三方发行 |
❌ 不支持:
SHIPPING- ANY_DAY_TICKET 不支持配送
4. 图片要求
- 至少 1 张图片(
images数组非空) - 建议包含封面图片(
setThumbnail: true) - 使用 Cloudinary 的
public_id作为图片 ID
5. 变体要求
- 至少 1 个变体
price必须提供(字符串或数字格式)inventoryQuantity必须为非负整数option如不提供会自动生成(基于options配置)
6. 税率配置
| 参数 | 说明 | 默认值 |
|---|---|---|
taxEnable |
是否启用税收 | false |
overrideEventTax |
是否覆盖 Event 税率 | false |
customTaxRate |
自定义税率 (0-100) | - |
7. 门票筛选配置 (atDoorTicketConfig)
| 字段 | 类型 | 说明 | |
|---|---|---|---|
includeTitleText |
`string \ | null` | 仅显示包含该标题的活动 |
excludeTitleText |
`string \ | null` | 排除包含该标题的活动 |
eventSelectionWindowHours |
`number \ | null` | 时间窗口(小时,≥ 1) |
相关接口
| 接口 | 方法 | 说明 |
|---|---|---|
/v2/products |
GET | 通用产品列表 |
/v2/products/any-day-tickets |
GET | ANY_DAY_TICKET 产品列表 |
/v2/products/:id |
GET | 获取单个产品详情 |
/v2/products/:id |
PUT | 更新产品 |
/v2/products/:id |
DELETE | 删除产品(软删除) |
/v2/products/batch |
POST | 批量导入产品 |
/product-event/v2/upcoming |
GET | 获取可选活动列表 |
相关文档
- [[POST /v2/products/any-day-tickets - 列出门票产品]] - ANY_DAY_TICKET 列表接口
- [[Product V2 Module]] - 产品 V2 模块文档
- [[KAT-10350-ANY-DAY-TICKET-DESIGN]] - ANY_DAY_TICKET 技术设计文档
- [[At-Door Ticket Config (KAT-10412)]] - 门票筛选配置说明
- [[Product Form (KAT-8634)]] - 产品表单功能
变更历史
| 版本 | 日期 | 变更内容 |
|---|---|---|
| v0.0.28 | 2026-02-XX | 初始版本(KAT-10350 Phase 6) |
| v0.0.29 | 2026-03-02 | 添加产品表单支持 (KAT-8634) |
| v0.0.30 | 2026-03-02 | 添加门票筛选配置 (KAT-10412) |
| v0.0.31 | 2026-03-02 | 添加数据库设计和 SQL 部分 |